우리는 왜 npm을 기피하게 되었을까

npm은 Node.js용으로 패키지를 설치하고 관리할 수 있는 도구를 만들기 위해 2010년에 등장했다.
npm 이전에는 패키지를 수동으로 복사하거나 git 저장소를 클론해서 의존성을 설치하는 등,
버전관리나 다른 사람이 만든 라이브러리를 공유하기 어려웠기 때문에 npm의 등장은 센세이션이었다.

NodeJS의 기본 패키지 매니저로 npm이 있었는데 pnpm이나 yarn같은 대체제가 등장하게 되었을까?


디스크 공간, 설치 속도 문제

npm(v2 이전)은 Nested Dependency Structure라는 구조로 의존성을 관리하는데

Nested Dependency Structure

그림에서 보듯, "Module_A@1.0.0을 직접 사용하고 있으면서",
"Module_B@1.0.0에서도 하위 의존성으로 Module_A@1.0.0을 두고 있다."

동일한 의존성이지만 npm은 Module_A@1.0.0을 두 번 설치한다.
마찬가지로 Module_D@1.0.0도 2번 설치된다.
이렇게 중복된 모듈이 있는 만큼, 디스크도 많이 차지하며, 의존성 설치 시간도 오래걸리게 되었다.

NPM의 노력 = 평탄화, 하지만 자충수

npm에서는 모듈 중복 문제를 해결하기 위해,
npm v3부터 의존성 트리를 평탄화하려고 했다.

중복이 되는 의존성은 node_modules의 최상단으로 hoisting하고,
또 사용되는 의존성이 있다면 루트에 있는 것을 공유해서 사용했다.

Flat Dependency Structure

그 결과로 Nested Dependency Structure에서 Flat Dependency Structure가 되었다.

평탄화된 의존성으로 디스크 공간 절약과 설치 속도의 감소라는 이득은 얻었지만.. 부작용으로 하위 의존성으로만 존재하던 의존성이 최상위까지 올라오게 되며 Ghost Dependency 문제가 나타났다.


Ghost Dependency(유령 의존성) 문제 등장

package-a와 package-b가 하위 의존성으로 lodash를 가지고 있었을 때 lodash는 상위로 호이스팅된다.

{
  // 내 package.json
  "dependencies": {
    "package-a": "^1.0.0",
    "package-b": "^2.0.0"
  }
}
// 평탄화로 설치된 node-modules
package-a
└── lodash (내가 직접 설치한 건 아님!)
package-b
└── lodash (내가 직접 설치한 건 아님!)
lodash

이렇게 되면서 내 프로젝트에선 lodash를 import할 수 있게 되었다.
package-a와 package-b가 하위 의존성으로 lodash를 가지고 있었을 때
내가 직접 설치하지 않은 의존성에 대해 import 할 수 있게 된 것이 Ghost Dependency다.

import _ from 'lodash';

내가 명시하지도 않은 라이브러리를 실수로 import해서 사용 했을 때,
만약 package-a의 내부 구조가 바뀌면서 lodash가 필요하지 않게 되었다면,
우리 프로젝트에서 lodash는 호이스팅 되지 않을 것이고
우리 프로젝트는 import할 수 없다고 에러를 내게 된다는 문제점이 있다.


의존성 충돌 문제 발생

누가 먼저 설치되었는지, 어떤 버전 충돌이 있었는지에 따라 호이스팅이 될수도 있고 안될 수도 있다.
그래서 로컬환경에선 정상 동작하던 것이 CI 환경에서는 import할 모듈을 찾지 못한다는 등
큰 문제가 발생하게 될 가능성이 있다.

또, 여러 패키지가 다른 버전 의존할 경우 어떤 버전이 올라올지 예측이 불가능했다.
package-a가 lodash v1을, package-b가 lodash v2를 사용한다고 했을 때,
호이스팅되는 것이 v1인지 v2인지 알수가 없다.

개발자들은 이런 예측 불가능한 상황을 만드는 npm을 떠나 대체제를 찾게 되었다.

하위의존성에 있는 의존성이 루트로 올라갔는데, 어떻게 기존의 하위 의존성을 식별할 수 있었을까?

Node에서 제공된 require로 모듈을 탐색하는데
현재 프로젝트에서 못찾으면, 상위로 올라가서 탐색하는 알고리즘을 가지고 있기 때문에 가능했다.
project-b 내부의 require('lodash')는 루트로 올라가서 가져오게 된다.